/*
 * Copyright (C) 2010 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.jb2011.ninepatch4j;

import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;

/**
 * The chunk information for a nine patch.
 *
 * This does not represent the bitmap, only the chunk info responsible for the
 * padding and the stretching areas.
 *
 * Since android.graphics.drawable.NinePatchDrawable and
 * android.graphics.NinePatch both deal with the nine patch chunk as a byte[],
 * this class is converted to and from byte[] through serialization.
 *
 * This is meant to be used with the NinePatch_Delegate in Layoutlib API 5+.
 */
public class NinePatchChunk implements Serializable {

	/** Generated Serial Version UID */
	private static final long serialVersionUID = -7353439224505296217L;

	private static final int[] sPaddingRect = new int[4];

	private boolean mVerticalStartWithPatch;
	private boolean mHorizontalStartWithPatch;

	private List<Rectangle> mFixed;
	private List<Rectangle> mPatches;
	private List<Rectangle> mHorizontalPatches;
	private List<Rectangle> mVerticalPatches;

	private Pair<Integer> mHorizontalPadding;
	private Pair<Integer> mVerticalPadding;

	/**
	 * Data computed during drawing.
	 */
	static final class DrawingData {
		private int mRemainderHorizontal;
		private int mRemainderVertical;
		private float mHorizontalPatchesSum;
		private float mVerticalPatchesSum;
	}

	/**
	 * Computes and returns the 9-patch chunks.
	 * 
	 * @param image
	 *            the image containing both the content and the control outer line.
	 * @return the {@link NinePatchChunk}.
	 */
	public static NinePatchChunk create(BufferedImage image) {
		NinePatchChunk chunk = new NinePatchChunk();
		chunk.findPatches(image);
		return chunk;
	}

	public void draw(BufferedImage image, Graphics2D graphics2D, int x, int y, int scaledWidth,
			int scaledHeight, int destDensity, int srcDensity) {

		boolean scaling = destDensity != srcDensity && destDensity != 0 && srcDensity != 0;

		if (scaling) {
			try {
				graphics2D = (Graphics2D) graphics2D.create();

				// scale and transform
				float densityScale = (float) destDensity / srcDensity;

				// translate/rotate the canvas.
				graphics2D.translate(x, y);
				graphics2D.scale(densityScale, densityScale);

				// sets the new drawing bounds.
				scaledWidth /= densityScale;
				scaledHeight /= densityScale;
				x = y = 0;

				// draw
				draw(image, graphics2D, x, y, scaledWidth, scaledHeight);
			} finally {
				graphics2D.dispose();
			}
		} else {
			// non density-scaled rendering
			draw(image, graphics2D, x, y, scaledWidth, scaledHeight);
		}
	}

	private void draw(BufferedImage image, Graphics2D graphics2D, int x, int y, int scaledWidth,
			int scaledHeight) {
		if (scaledWidth <= 1 || scaledHeight <= 1) {
			return;
		}

		Graphics2D g = (Graphics2D) graphics2D.create();
		g.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
				RenderingHints.VALUE_INTERPOLATION_BILINEAR);

		try {
			if (mPatches.size() == 0) {
				g.drawImage(image, x, y, scaledWidth, scaledHeight, null);
				return;
			}

			g.translate(x, y);
			x = y = 0;

			DrawingData data = computePatches(scaledWidth, scaledHeight);

			int fixedIndex = 0;
			int horizontalIndex = 0;
			int verticalIndex = 0;
			int patchIndex = 0;

			boolean hStretch;
			boolean vStretch;

			float vWeightSum = 1.0f;
			float vRemainder = data.mRemainderVertical;

			vStretch = mVerticalStartWithPatch;
			while (y < scaledHeight - 1) {
				hStretch = mHorizontalStartWithPatch;

				int height = 0;
				float vExtra = 0.0f;

				float hWeightSum = 1.0f;
				float hRemainder = data.mRemainderHorizontal;

				while (x < scaledWidth - 1) {
					Rectangle r;
					if (!vStretch) {
						if (hStretch) {
							r = mHorizontalPatches.get(horizontalIndex++);
							float extra = r.width / data.mHorizontalPatchesSum;
							int width = (int) (extra * hRemainder / hWeightSum);
							hWeightSum -= extra;
							hRemainder -= width;
							g.drawImage(image, x, y, x + width, y + r.height, r.x, r.y,
									r.x + r.width, r.y + r.height, null);
							x += width;
						} else {
							r = mFixed.get(fixedIndex++);
							g.drawImage(image, x, y, x + r.width, y + r.height, r.x, r.y,
									r.x + r.width, r.y + r.height, null);
							x += r.width;
						}
						height = r.height;
					} else {
						if (hStretch) {
							r = mPatches.get(patchIndex++);
							vExtra = r.height / data.mVerticalPatchesSum;
							height = (int) (vExtra * vRemainder / vWeightSum);
							float extra = r.width / data.mHorizontalPatchesSum;
							int width = (int) (extra * hRemainder / hWeightSum);
							hWeightSum -= extra;
							hRemainder -= width;
							g.drawImage(image, x, y, x + width, y + height, r.x, r.y,
									r.x + r.width, r.y + r.height, null);
							x += width;
						} else {
							r = mVerticalPatches.get(verticalIndex++);
							vExtra = r.height / data.mVerticalPatchesSum;
							height = (int) (vExtra * vRemainder / vWeightSum);
							g.drawImage(image, x, y, x + r.width, y + height, r.x, r.y,
									r.x + r.width, r.y + r.height, null);
							x += r.width;
						}

					}
					hStretch = !hStretch;
				}
				x = 0;
				y += height;
				if (vStretch) {
					vWeightSum -= vExtra;
					vRemainder -= height;
				}
				vStretch = !vStretch;
			}

		} finally {
			g.dispose();
		}
	}

	/**
	 * Fills the given array with the nine patch padding.
	 *
	 * @param padding
	 *            array of left, top, right, bottom padding
	 */
	public void getPadding(int[] padding) {
		padding[0] = mHorizontalPadding.mFirst; // left
		padding[2] = mHorizontalPadding.mSecond; // right
		padding[1] = mVerticalPadding.mFirst; // top
		padding[3] = mVerticalPadding.mSecond; // bottom
	}

	/**
	 * Returns the padding as an int[] describing left, top, right, bottom.
	 *
	 * This method is not thread-safe and returns an array owned by the
	 * {@link NinePatchChunk} class.
	 * 
	 * @return an internal array filled with the padding.
	 */
	public int[] getPadding() {
		getPadding(sPaddingRect);
		return sPaddingRect;
	}

	private DrawingData computePatches(int scaledWidth, int scaledHeight) {
		DrawingData data = new DrawingData();
		boolean measuredWidth = false;
		boolean endRow = true;

		int remainderHorizontal = 0;
		int remainderVertical = 0;

		if (mFixed.size() > 0) {
			int start = mFixed.get(0).y;
			for (Rectangle rect : mFixed) {
				if (rect.y > start) {
					endRow = true;
					measuredWidth = true;
				}
				if (!measuredWidth) {
					remainderHorizontal += rect.width;
				}
				if (endRow) {
					remainderVertical += rect.height;
					endRow = false;
					start = rect.y;
				}
			}
		}

		data.mRemainderHorizontal = scaledWidth - remainderHorizontal;
		data.mRemainderVertical = scaledHeight - remainderVertical;

		data.mHorizontalPatchesSum = 0;
		if (mHorizontalPatches.size() > 0) {
			int start = -1;
			for (Rectangle rect : mHorizontalPatches) {
				if (rect.x > start) {
					data.mHorizontalPatchesSum += rect.width;
					start = rect.x;
				}
			}
		} else {
			int start = -1;
			for (Rectangle rect : mPatches) {
				if (rect.x > start) {
					data.mHorizontalPatchesSum += rect.width;
					start = rect.x;
				}
			}
		}

		data.mVerticalPatchesSum = 0;
		if (mVerticalPatches.size() > 0) {
			int start = -1;
			for (Rectangle rect : mVerticalPatches) {
				if (rect.y > start) {
					data.mVerticalPatchesSum += rect.height;
					start = rect.y;
				}
			}
		} else {
			int start = -1;
			for (Rectangle rect : mPatches) {
				if (rect.y > start) {
					data.mVerticalPatchesSum += rect.height;
					start = rect.y;
				}
			}
		}

		return data;
	}

	/**
	 * Finds the 9-patch patches and padding from a {@link BufferedImage} image that
	 * contains both the image content and the control outer lines.
	 */
	private void findPatches(BufferedImage image) {
		// the size of the actual image content
		int width = image.getWidth() - 2;
		int height = image.getHeight() - 2;

		int[] row = null;
		int[] column = null;

		// extract the patch line. Make sure to start at 1 and be only as long as the
		// image content,
		// to not include the outer control line.
		row = GraphicsUtilities.getPixels(image, 1, 0, width, 1, row);
		column = GraphicsUtilities.getPixels(image, 0, 1, 1, height, column);

		boolean[] result = new boolean[1];
		Pair<List<Pair<Integer>>> left = getPatches(column, result);
		mVerticalStartWithPatch = result[0];

		result = new boolean[1];
		Pair<List<Pair<Integer>>> top = getPatches(row, result);
		mHorizontalStartWithPatch = result[0];

		mFixed = getRectangles(left.mFirst, top.mFirst);
		mPatches = getRectangles(left.mSecond, top.mSecond);

		if (mFixed.size() > 0) {
			mHorizontalPatches = getRectangles(left.mFirst, top.mSecond);
			mVerticalPatches = getRectangles(left.mSecond, top.mFirst);
		} else {
			if (top.mFirst.size() > 0) {
				mHorizontalPatches = new ArrayList<Rectangle>(0);
				mVerticalPatches = getVerticalRectangles(height, top.mFirst);
			} else if (left.mFirst.size() > 0) {
				mHorizontalPatches = getHorizontalRectangles(width, left.mFirst);
				mVerticalPatches = new ArrayList<Rectangle>(0);
			} else {
				mHorizontalPatches = mVerticalPatches = new ArrayList<Rectangle>(0);
			}
		}

		// extract the padding line. Make sure to start at 1 and be only as long as the
		// image
		// content, to not include the outer control line.
		row = GraphicsUtilities.getPixels(image, 1, height + 1, width, 1, row);
		column = GraphicsUtilities.getPixels(image, width + 1, 1, 1, height, column);

		top = getPatches(row, result);
		mHorizontalPadding = getPadding(top.mFirst);

		left = getPatches(column, result);
		mVerticalPadding = getPadding(left.mFirst);
	}

	private List<Rectangle> getVerticalRectangles(int imageHeight,
			List<Pair<Integer>> topPairs) {
		List<Rectangle> rectangles = new ArrayList<Rectangle>();
		for (Pair<Integer> top : topPairs) {
			int x = top.mFirst;
			int width = top.mSecond - top.mFirst;

			rectangles.add(new Rectangle(x, 0, width, imageHeight));
		}
		return rectangles;
	}

	private List<Rectangle> getHorizontalRectangles(int imageWidth,
			List<Pair<Integer>> leftPairs) {
		List<Rectangle> rectangles = new ArrayList<Rectangle>();
		for (Pair<Integer> left : leftPairs) {
			int y = left.mFirst;
			int height = left.mSecond - left.mFirst;

			rectangles.add(new Rectangle(0, y, imageWidth, height));
		}
		return rectangles;
	}

	private Pair<Integer> getPadding(List<Pair<Integer>> pairs) {
		if (pairs.size() == 0) {
			return new Pair<Integer>(0, 0);
		} else if (pairs.size() == 1) {
			if (pairs.get(0).mFirst == 0) {
				return new Pair<Integer>(pairs.get(0).mSecond - pairs.get(0).mFirst, 0);
			} else {
				return new Pair<Integer>(0, pairs.get(0).mSecond - pairs.get(0).mFirst);
			}
		} else {
			int index = pairs.size() - 1;
			return new Pair<Integer>(pairs.get(0).mSecond - pairs.get(0).mFirst,
					pairs.get(index).mSecond - pairs.get(index).mFirst);
		}
	}

	private List<Rectangle> getRectangles(List<Pair<Integer>> leftPairs,
			List<Pair<Integer>> topPairs) {
		List<Rectangle> rectangles = new ArrayList<Rectangle>();
		for (Pair<Integer> left : leftPairs) {
			int y = left.mFirst;
			int height = left.mSecond - left.mFirst;
			for (Pair<Integer> top : topPairs) {
				int x = top.mFirst;
				int width = top.mSecond - top.mFirst;

				rectangles.add(new Rectangle(x, y, width, height));
			}
		}
		return rectangles;
	}

	/**
	 * Computes a list of Patch based on a pixel line.
	 *
	 * This returns both the fixed areas, and the patches (stretchable) areas.
	 *
	 * The return value is a pair of list. The first list ({@link Pair#mFirst}) is
	 * the list of fixed area. The second list ({@link Pair#mSecond}) is the list of
	 * stretchable areas.
	 *
	 * Each area is defined as a Pair of (start, end) coordinate in the given line.
	 *
	 * @param pixels
	 *            the pixels of the control line. The line should have the same
	 *            length as the content (i.e. it should be stripped of the
	 *            first/last control pixel which are not used)
	 * @param startWithPatch
	 *            a boolean array of size 1 used to return the boolean value of
	 *            whether a patch (stretchable area) is first or not.
	 * @return
	 */
	private Pair<List<Pair<Integer>>> getPatches(int[] pixels, boolean[] startWithPatch) {
		int lastIndex = 0;
		int lastPixel = pixels[0];
		boolean first = true;

		List<Pair<Integer>> fixed = new ArrayList<Pair<Integer>>();
		List<Pair<Integer>> patches = new ArrayList<Pair<Integer>>();

		for (int i = 0; i < pixels.length; i++) {
			int pixel = pixels[i];
			if (pixel != lastPixel) {
				if (lastPixel == 0xFF000000) {
					if (first)
						startWithPatch[0] = true;
					patches.add(new Pair<Integer>(lastIndex, i));
				} else {
					fixed.add(new Pair<Integer>(lastIndex, i));
				}
				first = false;

				lastIndex = i;
				lastPixel = pixel;
			}
		}
		if (lastPixel == 0xFF000000) {
			if (first)
				startWithPatch[0] = true;
			patches.add(new Pair<Integer>(lastIndex, pixels.length));
		} else {
			fixed.add(new Pair<Integer>(lastIndex, pixels.length));
		}

		if (patches.size() == 0) {
			patches.add(new Pair<Integer>(1, pixels.length));
			startWithPatch[0] = true;
			fixed.clear();
		}

		return new Pair<List<Pair<Integer>>>(fixed, patches);
	}

	/**
	 * A pair of values.
	 *
	 * @param <E>
	 */
	/* package */ static class Pair<E> implements Serializable {
		/** Generated Serial Version UID */
		private static final long serialVersionUID = -2204108979541762418L;
		E mFirst;
		E mSecond;

		Pair(E first, E second) {
			mFirst = first;
			mSecond = second;
		}

		@Override
		public String toString() {
			return "Pair[" + mFirst + ", " + mSecond + "]";
		}
	}

}